P3960 [NOIP2017 提高组] 列队
题意
《\(900\) 亿 人 一 起 军 训》
Pomelorin 所在的方阵中有 \(n \times m\) 名学生(\(n\) 行 \(m\) 列)。初始时,第 \(i\) 行第 \(j\) 列 的学生的编号是 \((i-1)\times m + j\)。
一共发生了 \(q\) 件离队事件。每一次离队事件可以用数对 \((x,y)\)(满足 \(1 \le x \le n\),\(1 \le y \le m\))描述,表示第 \(x\) 行第 \(y\) 列的学生离队。在有学生离队后,队伍中出现了一个空位。之后方阵开始:
向左看齐。这时第一列保持不动,所有学生向左填补空缺。之后空位在第 \(x\) 行第 \(m\) 列。
向前看齐。这时第一行保持不动,所有学生向前填补空缺。之后空位在第 \(n\) 行第 \(m\) 列。
不能有两个或更多学生同时离队。离队的学生会自然地填补到第 \(n\) 行第 \(m\) 列。
计算每一次离队事件中,离队的同学的编号是多少。每一个同学的编号不会随着离队事件的发生而改变,在发生离队事件后方阵中同学的编号可能是乱序的。
数据规模与约定
测试点编号 | \(n\) | \(m\) | \(q\) | 其他约定 |
---|---|---|---|---|
\(1\sim 6\) | \(\le 10^3\) | \(\le 10^3\) | \(\le 500\) | 无 |
\(7\sim 10\) | \(\le 5\times 10^4\) | \(\le 5\times 10^4\) | \(\le 500\) | 无 |
\(11\sim 12\) | \(=1\) | \(\le 10^5\) | \(\le 10^5\) | 所有事件 \(x=1\) |
\(13\sim 14\) | \(=1\) | \(\le 3\times 10^5\) | \(\le 3\times 10^5\) | 所有事件 \(x=1\) |
\(15\sim 16\) | \(\le 3\times 10^5\) | \(\le 3\times 10^5\) | \(\le 3\times 10^5\) | 所有事件 \(x=1\) |
\(17\sim 18\) | \(\le 10^5\) | \(\le 10^5\) | \(\le 10^5\) | 无 |
\(19\sim 20\) | \(\le 3\times 10^5\) | \(\le 3\times 10^5\) | \(\le 3\times 10^5\) | 无 |
数据保证每一个事件满足 \(1 \le x \le n\),\(1 \le y \le m\)。
点击查看题解
题解
1-6 模拟
直接按照题意模拟。时间复杂度:\(O(nm)\)。可以获得 \(30\) 分。
1 | void solve1() |
7-10 离散化
我们发现,虽然 \(n\) 和 \(m\) 的范围很大,但 \(q\) 很小;换言之,这个方阵中大部分行都用不到。所以我们对行进行离散。由于除了最后一列,其它列对方阵变换没有实质性影响;换言之,如果采用模拟的方式,其他列是不参与模拟过程的,所以我们只保留最后一列即可。
经过离散化,形成如下结构(有图片,显得这篇题解完整):
然后直接按照题意模拟即可。
一个错误的离散化方法
1 | void solve2()//错误做法 |
错误原因:把列也离散化,其实只保留最后一列即可;将每行破坏,不能得出正确答案。
在行离散过程中,将最后一列破坏,不能得出正确答案。
一个繁琐的离散化方法
1 | void solve3() |
另外,我们考虑到,行与行直接不会互相影响。所以,我们不用考虑离散化后各行在数组中的位置。由此,可以得到下面的优化版代码。
1 | void solve5()//对 solve3 的优化 行与行之间互不影响 |
时间复杂度是 \(O(qm)\),结合算法 1 可以得到 \(50\) 分。
11-16 树状数组 + 二分
注意到此时的数据范围,对于所有询问,\(x=1\)。也就是说,只有第一行和最后一列有用(11-14 数据点本质上和 15-16 一样,故划归到 11-16)。
此时,我们可以将弯折的序列“掰直”,用一个数组维护。即,把第一行和最后一列压成一维数组。
1 | --- |
也就是说,维护的数列要支持两个操作:
- 查询第 \(x\) 个元素。
- 将元素移到数列最后面。
这里使用一个巧妙的方法:开一个标记数组,初始时 \(1\sim n+m-1\)(末尾)都为 \(1\),表示这个位置上有数;在发生离队事件后,相应的位置变为 \(0\),而当前数组末尾的下一个标记变为 \(1\),表示这个数移到末尾。
对这个数组求前缀和,其前缀和数组中的值正对应查询中的 \(y\)(\(y\) 意义同题意所述),下标正对应现数列中第 \(y\) 个数所在位置。
树状数组维护标记数组的前缀和;利用二分,根据标记数组前缀和,来寻找数列中第 \(y\) 个数所在位置。
时间复杂度是 \(O(N(\log N)^2)\),结合前面的算法可得到 \(80\) 分。
1 | void solve4() |
1-20 动态开点线段树
经过动态开点,可以节省一些空间,来足以通过本题。
我们要开 \(q+1\) 棵线段树:为每个询问开一个(好理解的话是每行),为最后一列开一个。
线段树中,只有叶子节点维护 data(题目中人的编号);各节点维护的信息还有 num(这个节点维护的元素个数)。
- update 操作:单点修改,修改叶子节点的 data 值。沿途中节点维护元素数加一;在行(或最后一列)的末尾插入即可。
- query 操作:单点查询,查询线段树中存储的第 rank 个值。沿途中节点维护元素数减一(根据题意,询问完后接着删除)。如果递归进入左子树,仍在左子树查询第 rank 个值;如果进入右子树,则在右子树查询第 (rank \(-\) 左子树元素个数) 个值。
对于每个询问,分两类讨论:
如果 \(y=m\):
只在最后一列修改。找到第 \(x\) 个数,输出并放到末尾。
否则:
先从第 \(x\) 行中找到第 \(y\) 个数,输出并放到最后一列末尾;把最后一列中第 \(x\) 个数放到第 \(x\) 行末尾。
时间复杂度是 \(O(n\log n)\)。本算法可得到 \(100\) 分。
代码
完整代码如下:
1 |
|